﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Dynamic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace Mvc4Demo.Core
{
    public class DynamicModel : DynamicObject
    {
        private static readonly ConcurrentDictionary<Type, Dictionary<string, PropertyInfo>> TypeProperties = new ConcurrentDictionary<Type, Dictionary<string, PropertyInfo>>();

        public DynamicModel(object model)
        {
            Model = model;
        }

        #region internam members

        internal Dictionary<string, PropertyInfo> Properties
        {
            get
            {
                var modelType = Model.GetType();

                return TypeProperties.GetOrAdd(modelType, t => t.GetProperties().ToDictionary(x => x.Name));
            }
        }

        #endregion

        public object Model { get; set; }

        public bool IsDirty { get; private set; }

        #region overriders

        public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result)
        {
            var propertyName = indexes[0] as string;
            if (propertyName == null)
                throw new InvalidOperationException();

            var info = Properties[propertyName];

            result = info.GetValue(Model, null);

            return true;

        }

        public override bool TrySetIndex(SetIndexBinder binder, object[] indexes, object value)
        {
            var propertyName = indexes[0] as string;
            if (propertyName == null)
                throw new InvalidOperationException();

            var info = Properties[propertyName];

            info.SetValue(Model, value);

            return true;
        }

        public override bool TryGetMember(GetMemberBinder binder, out object result)
        {
            if (Properties.ContainsKey(binder.Name))
            {
                var info = Properties[binder.Name];

                result = info.GetValue(Model, null);

                return true;
            }
            return base.TryGetMember(binder, out result);
        }

        public override bool TrySetMember(SetMemberBinder binder, object value)
        {
            if (Properties.ContainsKey(binder.Name))
            {
                var info = Properties[binder.Name];

                ValidatePropertyValue(binder.Name, info, value);

                SetValue(info, value);

                NotifyPropertyChanged(binder.Name);

                IsDirty = true;

                NotifyPropertyChanged("IsDirty");

                return true;
            }
            return base.TrySetMember(binder, value);
        }

        public override IEnumerable<string> GetDynamicMemberNames()
        {
            return Properties.Keys;
        }

        #endregion


        #region Implementation of INotifyPropertyChanged


        protected void NotifyPropertyChanged(String info)
        {

        }
        protected void RaisePropertyChanged<T>(Expression<Func<T>> property)
        {

            var expression = property.Body as MemberExpression;
            if (expression == null) return;
            var member = expression.Member;
        }

        #endregion

        #region Private methods

        private void SetValue(PropertyInfo info, object value)
        {
            var converter = TypeDescriptor.GetConverter(info.PropertyType);
            if (!converter.IsValid(value)) return;
            try
            {
                info.SetValue(Model, value, null);
            }
            catch (Exception)
            {
                var strValues = converter.ConvertToString(value);
                if (strValues == null) return;
                var newValue = converter.ConvertFromString(strValues);
                info.SetValue(Model, newValue, null);
            }
        }

        private void ValidatePropertyValue(string propertyName, PropertyInfo info, object value)
        {
            var properyName = GetPropertyDisplayName(propertyName, info);

            var validations = info.GetCustomAttributes(typeof(ValidationAttribute), true)
                .Select(x => x as ValidationAttribute);

            var validValues =
                validations.Select(x => x.GetValidationResult(value, GetValidationContext(properyName)))
                           .Where(x => x != ValidationResult.Success)
                           .ToList();

            if (validValues.Any())
            {
                throw new Exception(validValues.First().ErrorMessage);
            }
        }

        private ValidationContext GetValidationContext(string name)
        {
            return new ValidationContext(this, null, null)
            {
                DisplayName = name
            };
        }

        private static string GetPropertyDisplayName(string propertyName, PropertyInfo info)
        {
            var displayNameAttrs = info.GetCustomAttributes(typeof(DisplayAttribute), true)
                                       .Select(x => x as DisplayAttribute)
                                       .ToList();
            if (displayNameAttrs.Any())
            {
                propertyName = displayNameAttrs.First().GetName();
            }
            return propertyName;
        }

        #endregion
    }

}